﻿using System;

namespace Jellyfish.Virtu
{
	// ReSharper disable once UnusedMember.Global
	internal enum SectorSkew { None = 0, Dos, ProDos }

	internal sealed class DiskDsk : Disk525
	{
		private const int SecondaryBufferLength = 0x56;
		private const int Volume = 0xFE;

		private byte[] _trackBuffer;
		private int _trackOffset;
		private byte[] _primaryBuffer = new byte[0x100];
		private byte[] _secondaryBuffer = new byte[SecondaryBufferLength + 1];
		private int[] _sectorSkew;

		public DiskDsk(byte[] data, bool isWriteProtected, SectorSkew sectorSkew)
			: base(data, isWriteProtected)
		{
			_sectorSkew = SectorSkewMode[(int)sectorSkew];
		}

		public override void Sync(IComponentSerializer ser)
		{
			ser.Sync(nameof(_trackBuffer), ref _trackBuffer, false);
			ser.Sync(nameof(_trackOffset), ref _trackOffset);
			ser.Sync(nameof(_primaryBuffer), ref _primaryBuffer, false);
			ser.Sync(nameof(_secondaryBuffer), ref _secondaryBuffer, false);
			ser.Sync(nameof(_sectorSkew), ref _sectorSkew, false);
			base.Sync(ser);
		}

		public override void ReadTrack(int number, int fraction, byte[] buffer)
		{
			int track = number / 2;

			_trackBuffer = buffer;
			_trackOffset = 0;

			WriteNibble(0xFF, 48); // gap 0

			for (int sector = 0; sector < SectorCount; sector++)
			{
				WriteNibble(0xD5); // address prologue
				WriteNibble(0xAA);
				WriteNibble(0x96);

				WriteNibble44(Volume);
				WriteNibble44(track);
				WriteNibble44(sector);
				WriteNibble44(Volume ^ track ^ sector);

				WriteNibble(0xDE); // address epilogue
				WriteNibble(0xAA);
				WriteNibble(0xEB);
				WriteNibble(0xFF, 8);

				WriteNibble(0xD5); // data prologue
				WriteNibble(0xAA);
				WriteNibble(0xAD);

				WriteDataNibbles((track * SectorCount + _sectorSkew[sector]) * SectorSize);

				WriteNibble(0xDE); // data epilogue
				WriteNibble(0xAA);
				WriteNibble(0xEB);
				WriteNibble(0xFF, 16);
			}
		}

		public override void WriteTrack(int number, int fraction, byte[] buffer)
		{
			if (IsWriteProtected)
			{
				return;
			}

			int track = number / 2;

			_trackBuffer = buffer;
			_trackOffset = 0;
			int sectorsDone = 0;

			for (int sector = 0; sector < SectorCount; sector++)
			{
				if (!Read3Nibbles(0xD5, 0xAA, 0x96, 0x304))
				{
					break; // no address prologue
				}

				/*int readVolume = */
				ReadNibble44();

				int readTrack = ReadNibble44();
				if (readTrack != track)
				{
					break; // bad track number
				}

				int readSector = ReadNibble44();
				if (readSector > SectorCount)
				{
					break; // bad sector number
				}

				if ((sectorsDone & (0x1 << readSector)) != 0)
				{
					break; // already done this sector
				}

				if (ReadNibble44() != (Volume ^ readTrack ^ readSector))
				{
					break; // bad address checksum
				}

				if (ReadNibble() != 0xDE || ReadNibble() != 0xAA)
				{
					break; // bad address epilogue
				}

				if (!Read3Nibbles(0xD5, 0xAA, 0xAD, 0x20))
				{
					break; // no data prologue
				}

				if (!ReadDataNibbles((track * SectorCount + _sectorSkew[sector]) * SectorSize))
				{
					break; // bad data checksum
				}

				if (ReadNibble() != 0xDE || ReadNibble() != 0xAA)
				{
					break; // bad data epilogue
				}

				sectorsDone |= 0x1 << sector;
			}

			if (sectorsDone != 0xFFFF)
			{
				throw new InvalidOperationException("disk error"); // TODO: we should alert the user and "dump" a NIB
			}
		}

		private byte ReadNibble()
		{
			byte data = _trackBuffer[_trackOffset];
			if (_trackOffset++ == TrackSize)
			{
				_trackOffset = 0;
			}
			return data;
		}

		private bool Read3Nibbles(byte data1, byte data2, byte data3, int maxReads)
		{
			bool result = false;
			while (--maxReads > 0)
			{
				if (ReadNibble() != data1)
				{
					continue;
				}

				if (ReadNibble() != data2)
				{
					continue;
				}

				if (ReadNibble() != data3)
				{
					continue;
				}

				result = true;
				break;
			}
			return result;
		}

		private int ReadNibble44()
		{
			return ((ReadNibble() << 1) | 0x1) & ReadNibble();
		}

		private byte ReadTranslatedNibble()
		{
			return NibbleToByte[ReadNibble()];
		}

		private bool ReadDataNibbles(int sectorOffset)
		{
			byte y = SecondaryBufferLength;
			byte a = 0;
			do // fill and de-nibblize secondary buffer
			{
				a = _secondaryBuffer[--y] = (byte)(a ^ ReadTranslatedNibble());
			}
			while (y > 0);

			do // fill and de-nibblize secondary buffer
			{
				a = _primaryBuffer[y++] = (byte)(a ^ ReadTranslatedNibble());
			}
			while (y != 0);

			int checksum = a ^ ReadTranslatedNibble(); // should be 0

			var x = y = 0;
			do // decode data
			{
				if (x == 0)
				{
					x = SecondaryBufferLength;
				}
				a = (byte)((_primaryBuffer[y] << 2) | SwapBits[_secondaryBuffer[--x] & 0x03]);
				_secondaryBuffer[x] >>= 2;
				Data[sectorOffset + y] = a;
			}
			while (++y != 0);

			return (checksum == 0);
		}

		private void WriteNibble(int data)
		{
			_trackBuffer[_trackOffset++] = (byte)data;
		}

		private void WriteNibble(int data, int count)
		{
			while (count-- > 0)
			{
				WriteNibble(data);
			}
		}

		private void WriteNibble44(int data)
		{
			WriteNibble((data >> 1) | 0xAA);
			WriteNibble(data | 0xAA);
		}

		private void WriteDataNibbles(int sectorOffset)
		{
			byte a, x;

			for (x = 0; x < SecondaryBufferLength; x++)
			{
				_secondaryBuffer[x] = 0; // zero secondary buffer
			}

			byte y = 2;
			do // fill buffers
			{
				x = 0;
				do
				{
					a = Data[sectorOffset + --y];
					_secondaryBuffer[x] = (byte)((_secondaryBuffer[x] << 2) | SwapBits[a & 0x03]); // b1,b0 -> secondary buffer
					_primaryBuffer[y] = (byte)(a >> 2); // b7-b2 -> primary buffer
				}
				while (++x < SecondaryBufferLength);
			}
			while (y != 0);

			y = SecondaryBufferLength;
			do // write secondary buffer
			{
				WriteNibble(ByteToNibble[_secondaryBuffer[y] ^ _secondaryBuffer[y - 1]]);
			}
			while (--y != 0);

			a = _secondaryBuffer[0];
			do // write primary buffer
			{
				WriteNibble(ByteToNibble[a ^ _primaryBuffer[y]]);
				a = _primaryBuffer[y];
			}
			while (++y != 0);

			WriteNibble(ByteToNibble[a]); // data checksum
		}

		private static readonly byte[] SwapBits = { 0, 2, 1, 3 };

		private static readonly int[] SectorSkewNone =
		{
			0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF
		};

		private static readonly int[] SectorSkewDos =
		{
			0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4, 0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF
		};

		private static readonly int[] SectorSkewProDos =
		{
			0x0, 0x8, 0x1, 0x9, 0x2, 0xA, 0x3, 0xB, 0x4, 0xC, 0x5, 0xD, 0x6, 0xE, 0x7, 0xF
		};

		private static readonly int[][] SectorSkewMode =
		{
			SectorSkewNone, SectorSkewDos, SectorSkewProDos
		};

		private static readonly byte[] ByteToNibble =
		{
			0x96, 0x97, 0x9A, 0x9B, 0x9D, 0x9E, 0x9F, 0xA6, 0xA7, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB2, 0xB3,
			0xB4, 0xB5, 0xB6, 0xB7, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xCB, 0xCD, 0xCE, 0xCF, 0xD3,
			0xD6, 0xD7, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE5, 0xE6, 0xE7, 0xE9, 0xEA, 0xEB, 0xEC,
			0xED, 0xEE, 0xEF, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF
		};

#pragma warning disable SA1137

		private static readonly byte[] NibbleToByte =
		{
			// padding for offset (not used)
			0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
			0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
			0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
			0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
			0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
			0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
			0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
			0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
			0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
			0x90, 0x91, 0x92, 0x93, 0x94, 0x95,

			// nibble translate table
			                                    0x00, 0x01, 0x98, 0x99, 0x02, 0x03, 0x9C, 0x04, 0x05, 0x06,
			0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x07, 0x08, 0xA8, 0xA9, 0xAA, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
			0xB0, 0xB1, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0xB8, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
			0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0x1B, 0xCC, 0x1C, 0x1D, 0x1E,
			0xD0, 0xD1, 0xD2, 0x1F, 0xD4, 0xD5, 0x20, 0x21, 0xD8, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
			0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0x29, 0x2A, 0x2B, 0xE8, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
			0xF0, 0xF1, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0xF8, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F
		};

#pragma warning restore SA1137
	}
}
